package webhook

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"fmt"
	"github.com/wI2L/jsondiff"
	"gitverse.ru/synapse/kubelatte/pkg/api/common"
	"gitverse.ru/synapse/kubelatte/pkg/logs"
	"gitverse.ru/synapse/kubelatte/pkg/mutate"
	"gitverse.ru/synapse/kubelatte/pkg/storage"
	"gitverse.ru/synapse/kubelatte/pkg/validate"
	"io"
	"net/http"
	"os"
	"strings"
	"sync"

	"gopkg.in/yaml.v2"
	admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"

	"github.com/pkg/errors"
	"gitverse.ru/synapse/kubelatte/pkg/util"
	"gitverse.ru/synapse/kubelatte/pkg/webhook/config"
	"k8s.io/api/admission/v1beta1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/serializer"
)

var reloadChan = make(chan bool)

var (
	runtimeScheme = runtime.NewScheme()
	codecs        = serializer.NewCodecFactory(runtimeScheme)
	deserializer  = codecs.UniversalDeserializer()
)

type mutationStatus int

const (
	failedMutation    mutationStatus = 0
	skippedMutation   mutationStatus = 1
	succeededMutation mutationStatus = 2
)

type serverState int

type ServerError struct {
	State serverState
}

const (
	ServerReady serverState = iota
	ServerConfigsError
	ServerCertificatesError
	ServerPending
	ServerNotReady
)

var ServerStateText = map[serverState]string{
	ServerReady:             "OK",
	ServerConfigsError:      "trigger or mutating config problem",
	ServerCertificatesError: "server certificates problem",
	ServerPending:           "server busy",
	ServerNotReady:          "server ",
}

const (
	injectionStatusSkipped = "skipped"
	injectionStatusFailure = "failure"
	injectionStatusSuccess = "success"
)

func init() {
	_ = admissionregistrationv1beta1.AddToScheme(runtimeScheme)
	_ = corev1.AddToScheme(runtimeScheme)
}

type Server struct {
	tlsServer           *http.Server
	httpServer          *http.Server
	config              *config.WebhookConfig
	certificateReloader util.CertificateReloader
	state               serverState
	mux                 *sync.Mutex
}

// NewWebhookServer is a constructor for webhookServer
func NewWebhookServer(
	config *config.WebhookConfig,
	certificateReloader util.CertificateReloader,
	mux *sync.Mutex,
) *Server {
	srv := &Server{
		config:              config,
		certificateReloader: certificateReloader,
		mux:                 mux,
	}
	return srv
}

func (whsvr *Server) serve(w http.ResponseWriter, r *http.Request) {
	// verify the content type is accurate
	contentType := r.Header.Get("Content-Type")
	if contentType != "application/json" {
		logs.Errorf("api=serve, message=invalid Content-Type expect `application/json`, contentType=%s ", contentType)
		http.Error(w, "invalid Content-Type expect `application/json`", http.StatusUnsupportedMediaType)
		return
	}

	var body []byte

	if r.Body != nil {
		if data, err := io.ReadAll(r.Body); err == nil {
			body = data
		}
	}

	if len(body) == 0 {
		logs.Errorf("api=serve, message=empty body received, contentType=%s, headers=%s", contentType, r.Header)
		http.Error(w, "empty body", http.StatusBadRequest)
		return
	}

	prettyS := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(string(body), "\"", "'"), " ", ""), "\n", " ")
	logs.Infof("api=serve, message=incoming request, body=%s", prettyS)

	var admissionResponse *v1beta1.AdmissionResponse
	var statusForMutations map[string]mutationStatus

	ar := v1beta1.AdmissionReview{}
	_, _, err := deserializer.Decode(body, nil, &ar)

	if err != nil {
		logs.Errorf("api=serve, reason=deserializer.Decode, message=cannot decode body, err=%v", err)
		http.Error(w, "empty body", http.StatusBadRequest)
		admissionResponse = &v1beta1.AdmissionResponse{
			Result: &metav1.Status{
				Message: err.Error(),
			},
		}
	} else {

		arfields, _, err := parseRequest(ar.Request, body)
		if err != nil {
			logs.Errorf("api=serve, reason=deserializer.Decode, message=cannot decode body, err=%v", err)
			http.Error(w, "empty body", http.StatusBadRequest)
			admissionResponse = &v1beta1.AdmissionResponse{
				Result: &metav1.Status{
					Message: err.Error(),
				},
			}
		}
		patchOperation := mutate.MC.Mutate(arfields.Object, ar.Request.Object.Raw, arfields)

		validationSuccess := true
		var vErr error
		if strings.EqualFold(arfields.Kind.Kind, "pod") && len(patchOperation) > 0 {
			validationSuccess, vErr = ValidateMutation(patchOperation)
		}

		if validationSuccess {
			patchBytes, _ := json.Marshal(patchOperation)
			admissionResponse = &v1beta1.AdmissionResponse{
				Allowed: true,
				Patch:   patchBytes,
				PatchType: func() *v1beta1.PatchType {
					pt := v1beta1.PatchTypeJSONPatch
					return &pt
				}(),
			}
		} else {
			admissionResponse = &v1beta1.AdmissionResponse{
				Allowed: false,
				Result: &metav1.Status{
					Message: vErr.Error(),
				},
			}
		}

	}

	admissionReview := v1beta1.AdmissionReview{}

	if admissionResponse != nil {
		admissionReview.Response = admissionResponse
		if ar.Request != nil {
			admissionReview.Response.UID = ar.Request.UID
		}
	}

	resp, err := json.Marshal(admissionReview)

	if err != nil {
		setAllStatusesToFailed(statusForMutations)
		logs.Errorf("api=serve, reason=json.Marshal, message=could not encode response, err=%v", err)
		http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError)
	}

	logs.Infof("api=serve, message=ready to write response") // todo: redo

	if _, err := w.Write(resp); err != nil {
		setAllStatusesToFailed(statusForMutations)
		logs.Errorf("api=serve, reason=w.Write, message=could not write response, err=%v", err)
		http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
	}
}

func ValidateMutation(patchOperation []jsondiff.Operation) (bool, error) {
	if os.Getenv(config.ScopeValidation) == "true" || os.Getenv(config.ScopeValidation) == "" {
		if storage.Storage.GetScopes() == nil {
			logs.Errorf("api=serve, message=rule file not found")
			return false, fmt.Errorf("api=serve, message=rule file not found")
		}

		if storage.Storage.GetScopeItems(validate.Mutation) == nil {
			logs.Errorf("api=serve, message=without scope rules all mutattions forbidden")
			return false, fmt.Errorf("api=serve, message=without scope rules all mutations forbidden")
		}

		conv := validate.GetConvertor()
		validator := validate.Validator{}
		validator.SetRules(storage.Storage.GetScopeItems(validate.Mutation))

		for _, operation := range patchOperation {
			data := conv.Convert(operation)
			out, _ := json.Marshal(data)
			logs.Debugf("Check patch %s", out)
			if err := validate.CheckScope(data, storage.Storage.GetScopeItems(validate.Mutation)); err != nil {
				msg := fmt.Sprintf("%s for %s", err, operation.Path)
				logs.Errorf("api=serve, message=%s", msg)
				return false, fmt.Errorf("validation failed. Message: %s", msg)
			}
		}
	}
	return true, nil
}

func (whsvr *Server) validate(w http.ResponseWriter, r *http.Request) {
	if whsvr.state != ServerReady {
		w.WriteHeader(http.StatusServiceUnavailable)
		w.Write([]byte(ServerStateText[whsvr.state]))
		return
	}

	// verify the content type is accurate
	contentType := r.Header.Get("Content-Type")
	if contentType != "application/json" {
		logs.Errorf("api=validation, message=invalid Content-Type expect `application/json`, contentType=%s ", contentType)
		http.Error(w, "invalid Content-Type expect `application/json`", http.StatusUnsupportedMediaType)
		return
	}

	var body []byte

	if r.Body != nil {
		if data, err := io.ReadAll(r.Body); err == nil {
			body = data
		}
	}

	if len(body) == 0 {
		logs.Errorf("api=validation, message=empty body received, contentType=%s, headers=%s", contentType, r.Header)
		http.Error(w, "empty body", http.StatusBadRequest)
		return
	}

	prettyS := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(string(body), "\"", "'"), " ", ""), "\n", " ")
	logs.Info("api=validation, message=incoming request, body=", prettyS)

	ar := v1beta1.AdmissionReview{}
	_, _, err := deserializer.Decode(body, nil, &ar)

	admissionResponse := &v1beta1.AdmissionResponse{
		Allowed: true,
	}

	if err != nil {
		logs.Errorf("api=validation, reason=deserializer.Decode, message=cannot decode body, err=%v", err)
		http.Error(w, "empty body", http.StatusBadRequest)
		admissionResponse.Allowed = false
		admissionResponse.Result = &metav1.Status{
			Message: err.Error(),
		}
	} else {
		admissionResponse.UID = ar.Request.UID

		requestData, originalRequest, err := parseRequest(ar.Request, body)
		if err != nil {
			logs.Errorf("api=validation, reason=parseRequest, message=could not parse request, err=%v", err)
			http.Error(w, fmt.Sprintf("could not parse request: %v", err), http.StatusBadRequest)
			return
		}

		scope := storage.Storage.GetScopeItems(validate.Validation)
		if err = validate.ValidateResource(originalRequest, *requestData, scope); err != nil {
			admissionResponse.Allowed = false
			admissionResponse.Result = &metav1.Status{
				Message: fmt.Sprintf("validation webhook error: %v", err),
			}
		}
	}

	ar.Response = admissionResponse
	resp, err := json.Marshal(ar)

	if err != nil {
		logs.Errorf("api=validation, reason=json.Marshal, message=could not encode response, err=%v", err)
		http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError)
	}

	logs.Infof("api=validation, message=ready to write response, result=%t", ar.Response.Allowed)

	if _, err := w.Write(resp); err != nil {
		logs.Errorf("api=validation, reason=w.Write, message=could not write response, err=%v", err)
		http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
	}
}

func parseRequest(request *v1beta1.AdmissionRequest, body []byte) (*common.ARFields, map[string]interface{}, error) {
	var obj map[string]interface{}
	if request.Operation != v1beta1.Delete && request.Object.Raw != nil {
		err := json.Unmarshal(request.Object.Raw, &obj)
		if err != nil {
			return nil, nil, err
		}
	}

	var oldObj map[string]interface{}
	if request.Operation != v1beta1.Create && request.OldObject.Raw != nil {
		err := json.Unmarshal(request.OldObject.Raw, &oldObj)
		if err != nil {
			return nil, nil, err
		}
	}
	var original map[string]interface{}
	err := json.Unmarshal(body, &original)
	if err != nil {
		return nil, nil, err
	}

	origlRequest, ok := original["request"]
	if !ok {
		logs.Debugf("there is no \"request\" field in body")
	}

	originalRequest, ok := origlRequest.(map[string]interface{})

	return &common.ARFields{
		Kind:      request.Kind,
		Namespace: request.Namespace,
		UserInfo:  common.ARUserInfo{Username: request.UserInfo.Username},
		Operation: request.Operation,
		Object:    obj,
		OldObject: oldObj,
	}, originalRequest, nil
}

func (whsvr *Server) immutable(w http.ResponseWriter, r *http.Request) {
	var body []byte

	if r.Body != nil {
		if data, err := io.ReadAll(r.Body); err == nil {
			body = data
		}
	}

	prettyS := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(string(body), "\"", "'"), " ", ""), "\n", " ")
	logs.Infof("api=immutable, message=incoming request, body=%s", prettyS)

	ar := v1beta1.AdmissionReview{}
	_, _, _ = deserializer.Decode(body, nil, &ar)

	admissionResponse := &v1beta1.AdmissionResponse{
		Allowed: false,
		Result: &metav1.Status{
			Message: fmt.Sprintf("Immutable resource"),
		},
	}

	admissionResponse.UID = ar.Request.UID
	resp, _ := json.Marshal(ar)

	if _, err := w.Write(resp); err != nil {
		logs.Errorf("api=immutable, reason=w.Write, message=could not write response, err=%v", err)
		http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
	}
}

func (whsvr *Server) healthz(w http.ResponseWriter, _ *http.Request) {
	w.WriteHeader(http.StatusOK)
}

func (whsvr *Server) liveness(w http.ResponseWriter, _ *http.Request) {
	w.WriteHeader(http.StatusOK)
}

func (whsvr *Server) readyness(w http.ResponseWriter, _ *http.Request) {
	w.WriteHeader(http.StatusOK)
}

func (whsvr *Server) Set(s serverState) {
	logs.Infof("Change server status to:%s ", ServerStateText[s])
	whsvr.state = s
}

func (whsvr *Server) updateStatusHndlr(w http.ResponseWriter, r *http.Request) {
	statusData, err := yaml.Marshal(util.StatusInfo)
	if err != nil {
		_, err = w.Write([]byte(err.Error()))
		if err != nil {
			logs.Errorf("api=updateStatusHndlr, err=%s", err.Error())
			return
		}
		logs.Errorf("api=updateStatusHndlr, err=%s", err.Error())
		return
	}
	_, err = w.Write(statusData)
	if err != nil {
		logs.Errorf("api=updateStatusHndlr, err=%s", err.Error())
		return
	}
}

func (whsvr *Server) Start() (chan bool, chan bool, chan bool, error) {

	wconfig := whsvr.config

	var tlsConfig *tls.Config
	var startTLS bool

	if wconfig.TLSPort != 0 {
		crt := wconfig.CertFilePath
		key := wconfig.KeyFilePath
		if crt == "" {
			return nil, nil, nil, errors.New("api=Start, reason=wconfig.CertFilePath, err=since the TLS port is set, you need to specify the file containing the x509 Certificate for HTTPS --cert-file-path or SERVER_KEY env")
		}
		if key == "" {
			return nil, nil, nil, errors.New("api=Start, reason=wconfig.KeyFilePath, err=since the TLS port is set, you need to specify the file containing the x509 private key --key-file-path or SERVER_CERT env")
		}
		if _, err := os.OpenFile(crt, os.O_RDONLY, 0644); errors.Is(err, os.ErrNotExist) {
			return nil, nil, nil, errors.Errorf("api=Start, reason=wconfig.CertFilePath, err=certificate file %s dont exist", crt)
		}
		if _, err := os.OpenFile(key, os.O_RDONLY, 0600); errors.Is(err, os.ErrNotExist) {
			return nil, nil, nil, errors.Errorf("api=Start, reason=wconfig.KeyFilePath, err=key file %s dont exist", key)
		}

		startTLS = true
	}

	var doneListeningTLSChannel chan bool

	isTLS := whsvr.certificateReloader != nil

	if startTLS {
		if isTLS {
			err := whsvr.certificateReloader.Start()
			if err != nil {
				logs.Errorf("api=Start, reason=certReloader.Start, certFilePath=%q, keyFilePath=%q, caFilePath=%q, err=%v", wconfig.CertFilePath, wconfig.KeyFilePath, wconfig.CaFilePath, err)
				return nil, nil, nil, errors.Errorf("api=Start, reason=certReloader.Start, certFilePath=%q, keyFilePath=%q, caFilePath=%q, err=%v", wconfig.CertFilePath, wconfig.KeyFilePath, wconfig.CaFilePath, err)
			}

			tlsConfig = &tls.Config{
				GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
					return whsvr.certificateReloader.GetCertificate()
				},
			}
			if !wconfig.AllowDeprecatedTLSConfig {
				tlsConfig.MinVersion = tls.VersionTLS12
				tlsConfig.CipherSuites = []uint16{
					tls.TLS_AES_256_GCM_SHA384,
					tls.TLS_AES_128_GCM_SHA256,
					tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
					tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
					tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
					tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
				}
			}

			if wconfig.CaFilePath != "" {
				caCert, err := os.ReadFile(wconfig.CaFilePath)
				if err != nil {
					logs.Errorf("api=Start, reason=ioutil.ReadFile, caFilePath=%q, err=%v", wconfig.CaFilePath, err)
					return nil, nil, nil, errors.Errorf("api=Start, reason=ioutil.ReadFile, caFilePath=%q, err=%v", wconfig.CaFilePath, err)
				}
				caCertPool := x509.NewCertPool()
				caCertPool.AppendCertsFromPEM(caCert)
				tlsConfig.RootCAs = caCertPool
			}
		}
	}

	// Define routing for requests to the server.
	router := http.NewServeMux()
	logs.ChangeDebugLevelServerStart(fmt.Sprintf(":%v", wconfig.HTTPPort), router)
	if util.IsMutatorRole() || util.IsFullRole() {
		logs.Info("Run templatter in MUTATOR role")
		router.HandleFunc("/mutate", whsvr.serve)
	}
	if util.IsValidatorRole() || util.IsFullRole() {
		logs.Info("Run templatter in VALIDATOR role")
		router.HandleFunc("/validate", whsvr.validate)
	}
	router.HandleFunc("/validate/tri", whsvr.immutable)
	router.HandleFunc("/healthz", whsvr.healthz)
	router.HandleFunc("/liveness", whsvr.liveness)
	router.HandleFunc("/readyness", whsvr.readyness)
	router.HandleFunc("/status", whsvr.updateStatusHndlr)

	if startTLS {
		// We create two servers: one that serves https requests, and another that serves http requests only.
		whsvr.tlsServer = &http.Server{
			Addr:      fmt.Sprintf(":%v", wconfig.TLSPort),
			TLSConfig: tlsConfig,
		}
		whsvr.tlsServer.Handler = router

		// Channel to indicate when the server stopped listening for some reason
		doneListeningTLSChannel := make(chan bool)

		// start webhook server in new routine
		go func() {
			if isTLS {
				if err := whsvr.tlsServer.ListenAndServeTLS("", ""); err != nil {
					logs.Errorf("api=Start, message=failed to listen and serve webhook server: %v", err)
				}
				doneListeningTLSChannel <- true
			}
		}()

	}

	whsvr.httpServer = &http.Server{
		Addr: fmt.Sprintf(":%v", wconfig.HTTPPort),
	}

	whsvr.httpServer.Handler = router

	// Channel to indicate when the server stopped listening for some reason
	doneListeningHTTPChannel := make(chan bool)

	logs.Info("api=Start, message=start http webhook server")

	go func() {
		if err := whsvr.httpServer.ListenAndServe(); err != nil {
			logs.Errorf("api=Start, reason='failed to listen and serve webhook HTTP server: %v'", err)
		}
		doneListeningHTTPChannel <- true
	}()

	return doneListeningTLSChannel, doneListeningHTTPChannel, reloadChan, nil
}

func (whsvr *Server) Stop() {
	logs.Infof("api=Stop, reason='shutting down webhook server gracefully...'")
	if whsvr.tlsServer != nil {
		whsvr.tlsServer.Shutdown(context.Background())
	}
	if whsvr.httpServer != nil {
		whsvr.httpServer.Shutdown(context.Background())
	}
	if whsvr.certificateReloader != nil {
		if whsvr.certificateReloader.IsRunning() {
			whsvr.certificateReloader.Stop()
		}
	}
}

func setAllStatusesToFailed(statusForMutations map[string]mutationStatus) {
	if statusForMutations != nil {
		for mutationConfig := range statusForMutations {
			statusForMutations[mutationConfig] = failedMutation
		}
	}
}
